package top.fullj.id;

import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;
import top.fullj.bean.ToString;

import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * @author bruce.wu
 * @since 2021/8/2 20:00
 */
public class SnowflakeId {

    //new Date(2020, 0, 1, 0, 0, 0).getTime()
    private static final long DEFAULT_EPOCH = 1577808000000L;

    // bits of sequence
    private static final int DEFAULT_BITS_SEQ = 10;
    // bits of worker id
    private static final int DEFAULT_BITS_PID = 5;
    // bits of data center id
    private static final int DEFAULT_BITS_MID = 5;

    public static Config defaultConfig() {
        return config(DEFAULT_EPOCH, DEFAULT_BITS_SEQ, DEFAULT_BITS_PID, DEFAULT_BITS_MID);
    }

    public static Config config(long epoch, int bitsSeq, int bitsPid, int bitsMid) {
        return new Config(epoch, bitsSeq, bitsPid, bitsMid);
    }

    private final Date ts;
    private final long mid;
    private final long pid;
    private final long seq;

    public SnowflakeId(String idStr) {
        this(idStr, defaultConfig());
    }

    public SnowflakeId(String idStr, Config config) {
        Preconditions.checkNotNull(config);
        Preconditions.checkNotNull(idStr);
        Preconditions.checkArgument(idStr.length() == 16);
        ByteBuffer buffer = ByteBuffer.wrap(BaseEncoding.base16().decode(idStr));
        long id = buffer.getLong();
        long timestamp = (id >> config.SHIFT_TS) + config.EPOCH;
        this.ts = new Date(timestamp);
        this.mid = (id >> config.SHIFT_MID) & config.MASK_MID;
        this.pid = (id >> config.SHIFT_PID) & config.MASK_PID;
        this.seq = id & config.MASK_SEQ;
    }

    @Override
    public String toString() {
        return ToString.toStringHelper(this)
                .add("ts", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(ts))
                .add("mid", mid)
                .add("pid", pid)
                .add("seq", seq)
                .toString();
    }

    public static class Config {

        final long EPOCH;

        final int BITS_SEQ;
        final int BITS_PID;
        final int BITS_MID;

        final int SHIFT_PID;
        final int SHIFT_MID;
        final int SHIFT_TS;

        final int MASK_SEQ;
        final int MASK_PID;
        final int MASK_MID;

        Config(long epoch, int bitsSeq, int bitsPid, int bitsMid) {
            this.EPOCH = epoch;
            this.BITS_SEQ = bitsSeq;
            this.BITS_PID = bitsPid;
            this.BITS_MID = bitsMid;

            this.SHIFT_PID = bitsSeq;
            this.SHIFT_MID = bitsSeq + bitsPid;
            this.SHIFT_TS = bitsSeq + bitsPid + bitsMid;

            this.MASK_SEQ = (1 << bitsSeq) - 1;
            this.MASK_PID = (1 << bitsPid) - 1;
            this.MASK_MID = (1 << bitsMid) - 1;
        }

        @Override
        public String toString() {
            return ToString.toStringHelper(this)
                    .add("EPOCH", EPOCH)
                    .add("BITS_SEQ", BITS_SEQ)
                    .add("BITS_PID", BITS_PID)
                    .add("BITS_MID", BITS_MID)
                    .add("SHIFT_PID", SHIFT_PID)
                    .add("SHIFT_MID", SHIFT_MID)
                    .add("SHIFT_TS", SHIFT_TS)
                    .add("MASK_SEQ", MASK_SEQ)
                    .add("MASK_PID", MASK_PID)
                    .add("MASK_MID", MASK_MID)
                    .toString();
        }
    }

    public static class Generator implements IdGenerator {

        // data center id
        private final long mid;
        // worker id
        private final long pid;
        // sequence
        private volatile long seq;
        // timestamp
        private volatile long timestamp;

        private final Config config;

        public Generator() {
            this(defaultConfig());
        }

        public Generator(Config config) {
            this(config, new SecureRandom());
        }

        private Generator(Config config, Random rnd) {
            this(config, rnd.nextInt(config.MASK_PID + 1), rnd.nextInt(config.MASK_MID + 1));
        }

        public Generator(long pid, long mid) {
            this(defaultConfig(), pid, mid);
        }

        public Generator(Config config, long pid, long mid) {
            this.config = config;
            this.mid = (mid & config.MASK_MID) << config.SHIFT_MID;
            this.pid = (pid & config.MASK_PID) << config.SHIFT_PID;
        }

        @Override
        public String next() {
            long id;

            synchronized (this) {
                long ts = System.currentTimeMillis();
                if (ts < this.timestamp) {
                    throw new RuntimeException("time back");
                }

                if (ts == this.timestamp) {
                    seq = (seq + 1) & config.MASK_SEQ;
                    if (seq == 0) {
                        ts = nextTs(this.timestamp);
                    }
                } else {
                    seq = 0;
                }

                id = ((ts - config.EPOCH) << config.SHIFT_TS) | mid | pid | seq;
                this.timestamp = ts;
            }

            ByteBuffer buffer = ByteBuffer.allocate(8);
            buffer.putLong(id);
            return BaseEncoding.base16().encode(buffer.array());
        }

        private long nextTs(long latest) {
            long ts = System.currentTimeMillis();
            while (ts <= latest) {
                ts = System.currentTimeMillis();
            }
            return ts;
        }

    }

}
